﻿////////////////////////////////////////////////////////////////////////////////////////////////////
//
//  © 2012 Stanislav S. Yarmonov.
//
//  TODO:LICENSE
//     
////////////////////////////////////////////////////////////////////////////////////////////////////
using System;
using System.IO;
using System.Text;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using Pandora.Data;
using Pandora.Data.Common;
using Pandora.Development.Parser;

namespace Pandora.Development
{
    public class SqlScripter
    {
        #region -- Grammar -----------------------------------------------------------------------
        public static Parse<char, char> Ctrl = Parsers.Token<char>(c => c == '$');
        
        public static Parse<char, char> AnyChar = Parsers.Any<char>();

        public static Parse<char, ScriptNode> Text = SqlScripter
            .AnyChar
            .Not(SqlScripter.Ctrl)
            .Many(min: 1)
            .Select(result => new TextNode(result) as ScriptNode)
        ;

        public static Parse<char, string> FilePath = SqlScripter
            .AnyChar
            .Not(Parsers.Token<char>(')'))
            .Many(min: 1)
            .Select(args => new string(args.ToArray()))
        ;

        public static Parse<char, ScriptNode> Include =
            from directive in Parsers.Tokens("include")
            from open in Parsers.Token('(')
            from file in SqlScripter.FilePath
            from close in Parsers.Token(')')
            select new IncludeNode(file) as ScriptNode
        ;

        public static Parse<char, ScriptNode> IncludeText =
            from directive in Parsers.Tokens("text")
            from open in Parsers.Token('(')
            from file in SqlScripter.FilePath
            from close in Parsers.Token(')')
            select new IncludeNode(file, true) as ScriptNode
        ;

        public static Parse<char, ScriptNode> Quoted =
            Parsers.Token<char>('$')
            .Select(result => new TextNode("$") as ScriptNode)
        ;

        public static Parse<char, ScriptNode> Directive = SqlScripter
            .Ctrl
            .And(parser => SqlScripter
                .Include
                .Or(SqlScripter.IncludeText)
                .Xor(SqlScripter.Quoted)
            )
        ;

        public static Parse<char, ScriptNode> Element = SqlScripter
            .Text
            .Xor(SqlScripter.Directive)
        ;

        public static Parse<char, IEnumerable<ScriptNode>> Elements = SqlScripter
            .Element
            .Many()
        ;
        #endregion -------------------------------------------------------------------------------

        #region -- Nodes -------------------------------------------------------------------------
        public abstract class ScriptNode
        {
            private SqlScripter _Scripter = null;
            public SqlScripter Scripter
            {
                get
                {
                    return _Scripter;
                }
                internal set
                {
                    _Scripter = value;
                }
            }

            public abstract void Execute(TextWriter writer);
        }

        private class TextNode : ScriptNode
        {
            private string _Text = null;

            public override void Execute(TextWriter writer)
            {
                writer.Write(_Text);
            }

            public TextNode(IEnumerable<char> chars)
            {
                _Text = new string(chars.ToArray());
            }
        }

        private abstract class DirectiveNode : ScriptNode
        {

        }

        private class IncludeNode : DirectiveNode
        {
            private string _Path = null;
            private bool _Process = false;

            public override void Execute(TextWriter writer)
            {
                string path = Environment.ExpandEnvironmentVariables(_Path);
                if (!Path.IsPathRooted(path))
                {
                    path = Path.Combine(this.Scripter.CurrentDirectory.FullName, path);
                }

                FileInfo file = new FileInfo(path);
                if (!file.Exists)
                {
                    throw new FileNotFoundException(file.FullName);
                }

                this.Scripter.ProcessFile(file, writer, _Process);
            }

            public IncludeNode(string path, bool process = false)
            {
                _Path = path;
                _Process = process;
            }
        }
        #endregion -------------------------------------------------------------------------------

        private FileInfo _ScriptFile = null;
        public FileInfo ScriptFile
        {
            get
            {
                return _ScriptFile;
            }
        }

        private DirectoryInfo _CurrentDirectory = null;
        public DirectoryInfo CurrentDirectory
        {
            get
            {
                return _CurrentDirectory;
            }
        }

        private void ProcessFile(FileInfo file, TextWriter writer, bool process)
        {
            DirectoryInfo directory = _CurrentDirectory;
            _CurrentDirectory = file.Directory;

            string text = file.OpenText().ReadToEnd();

            if (process)
            {
                text = text.Replace("'", "''");
            }

            StringInput input = new StringInput(text);
            SqlScripter.Elements(input)
                .OnFailure(failure =>
                {
                    throw new Exception(failure.Error.Message);
                })
                .OnSuccess(success =>
                {
                    foreach (ScriptNode node in success.Result)
                    {
                        node.Scripter = this;
                        node.Execute(writer);
                    }
                })
            ;

            _CurrentDirectory = directory;
        }

        public SqlScripter Execute(TextWriter writer)
        {
            this.ProcessFile(_ScriptFile, writer, false);

            return this;
        }

        public SqlScripter Execute(string file)
        {
            if (Path.IsPathRooted(file))
            {
                file = Path.Combine(_ScriptFile.DirectoryName, file);
            }

            if (File.Exists(file))
            {
                File.Delete(file);
            }

            using (StreamWriter writer = new StreamWriter(file, false, Encoding.UTF8))
            {
                return this.Execute(writer);
            }
        }

        private SqlScripter(string path)
        {
            _ScriptFile = new FileInfo(path);
        }

        public static SqlScripter Parse(string path)
        {
            return new SqlScripter(path);
        }
    }
}
